﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.GamerServices;

namespace Tomahawk.Runtime.Networking
{

    public abstract class NetworkService
    {
        protected NetworkSession currentNetSession;
        private NetworkSession createdNetSession;
        private NetworkSession joinedNetSession;
        protected AvailableNetworkSessionCollection foundNetSessions;

        public NetworkService()
        {
            //default state
            this.currentState = NetServiceStates.Idle;
        }

        private float serverTimeOut = -1.0f;
        private bool checkServerTimeOut = false;

        #region "Session configuration"

        /// <summary>
        /// Default sections for game configuration
        /// </summary>
        public enum DefaultSessionPropertiesSections
        {
            GameType = 0,

            //use this to build upon this enum
            MAX_DEFAULT_SESSION_PROPERTIES_SECTIONS
        }

        /// <summary>
        /// Default (common) gametypes for GameType
        /// </summary>
        public enum DefaultGameTypes
        {
            Coop = 0,
            TeamCoop,
            CaptureTheFlag,
            DeathMatch,
            TeamDeathMatch,

            //use this to build upon this enum
            MAX_DEFAULT_GAME_TYPES
        }

        int configMaxLocalGamers = 1;

        int configPrivateGamerSlots = 0;

        int configMaxGamers = 16;

        /// <summary>
        /// Override this to provide a customization of the gamer configurations
        /// </summary>
        protected abstract void getConfigGamers(out int maxLocalGamers, out int privateGamerSlots, out int maxGamers);

        /// <summary>
        /// Override this to provide game session properties
        /// </summary>
        protected abstract NetworkSessionProperties getNetworkSessionProperties();

        /// <summary>
        /// Get the network session type.
        /// By default: On XBOX_DEBUG y PC se usa SystemLink. En XBOX_RELEASE se usa PlayerMatch
        /// </summary>
        protected virtual NetworkSessionType getNetworkSessionType()
        {
#if XBOX
#if DEBUG
            return NetworkSessionType.SystemLink;
#else
            return NetworkSessionType.PlayerMatch;
#endif
#else
            return NetworkSessionType.SystemLink;
#endif
        }

        #endregion

        #region "State Management"

        /// <summary>
        /// Available states for the network service
        /// </summary>
        public enum NetServiceStates
        {
            Idle,
            CreatingGame,
            SearchingGames,
            GamesFound,
            JoiningGame,
            InLobby,
            Starting,
            Playing
        }

        /// <summary>
        /// Dispose the net session and change state to idle
        /// </summary>
        protected void disposeSession()
        {
            if (this.currentNetSession != null)
            {
                //dispose the session
                try
                {
                    this.currentNetSession.Dispose();
                }
                catch (Exception) { }

                this.changeState(NetServiceStates.Idle);

                this.currentNetSession = null;
                this.createdNetSession = null;
                this.joinedNetSession = null;
            }
        }

        /// <summary>
        /// Current state of the network service
        /// </summary>
        private NetServiceStates currentState;

        /// <summary>
        /// Change the state of the network service
        /// </summary>
        protected void changeState(NetServiceStates newState)
        {
            switch (newState)
            {

                //
                // The sesion is created and in lobby mode
                //

                case NetServiceStates.InLobby:

                    if (this.createdNetSession != null) this.currentNetSession = this.createdNetSession;
                    if (this.joinedNetSession != null) this.currentNetSession = this.joinedNetSession;

                    //subscribe events
                    this.currentNetSession.GameStarted += new EventHandler<GameStartedEventArgs>(currentNetSession_GameStarted);
                    this.currentNetSession.SessionEnded += new EventHandler<NetworkSessionEndedEventArgs>(currentNetSession_SessionEnded);
                    this.currentNetSession.HostChanged += new EventHandler<HostChangedEventArgs>(currentNetSession_HostChanged);
                    this.currentNetSession.GamerLeft += new EventHandler<GamerLeftEventArgs>(currentNetSession_GamerLeft);
                    this.currentNetSession.GamerJoined += new EventHandler<GamerJoinedEventArgs>(currentNetSession_GamerJoined);
                    this.currentNetSession.GameEnded += new EventHandler<GameEndedEventArgs>(currentNetSession_GameEnded);

                    //notify session is created
                    this.OnSessionCreated();
                    break;

                //
                // The sesion had gone to playing state
                //

                case NetServiceStates.Playing:
                    this.OnGameStarted();
                    break;

                //
                // Iddle
                // 

                case NetServiceStates.Idle:
                    break;

            }

            this.currentState = newState;

            //debug
            Engine.Instance.DebugConsole.Warn("Network Service: state changed to: " + this.currentState.ToString());
        }

        #endregion

        #region "Network session event handlers"

        #region "GAME END"

        /// <summary>
        /// Handle a GAME ENDED notification from the session object
        /// </summary>
        void currentNetSession_GameEnded(object sender, GameEndedEventArgs e)
        {
            //debug info
            Engine.Instance.DebugConsole.Warn("Network Service: Game ended!");

            //back to idle state
            this.changeState(NetServiceStates.InLobby);

            //notify client code
            this.OnGameEnded(e);
        }

        /// <summary>
        /// Override this to provide specific logic for network game end
        /// </summary>
        protected virtual void OnGameEnded(GameEndedEventArgs e)
        {
        }

        #endregion

        #region "GAMER JOINED"

        /// <summary>
        /// Handle a GAMER JOINED notification from the session object
        /// </summary>
        void currentNetSession_GamerJoined(object sender, GamerJoinedEventArgs e)
        {
            //debug info
            Engine.Instance.DebugConsole.Warn(
                String.Format("Network Service: Gamer {0} joined!", e.Gamer.Gamertag));

            if (!e.Gamer.IsHost)
            {
                //no timeout once a player had joined
                this.checkServerTimeOut = false;
            }

            //notify client code
            this.OnGamerJoined(e);
        }

        /// <summary>
        /// Override this to provice specific gamer joined code
        /// </summary>
        protected virtual void OnGamerJoined(GamerJoinedEventArgs e)
        {
        }

        #endregion

        #region "GAMER LEFT"

        /// <summary>
        /// Handle a GAMER LEFT notification from the session object
        /// </summary>
        void currentNetSession_GamerLeft(object sender, GamerLeftEventArgs e)
        {
            //debug info
            Engine.Instance.DebugConsole.Warn(
                String.Format("Network Service: Gamer {0} Left!", e.Gamer.Gamertag));

            //notify client code
            this.OnGamerLeft(e);
        }

        /// <summary>
        /// Override this to provide specific gamer left logic
        /// </summary>
        protected virtual void OnGamerLeft(GamerLeftEventArgs e)
        {
        }

        #endregion

        #region "HOST CHANGED"

        /// <summary>
        /// Handle a HOST CHANGED notification from the network session
        /// </summary>
        void currentNetSession_HostChanged(object sender, HostChangedEventArgs e)
        {
            //debug info
            Engine.Instance.DebugConsole.Warn("Network Service: Host changed to: " + e.NewHost.Gamertag);

            //ANYTHING TO DO HERE??

            //notify client code
            this.OnHostChanged(e);
        }

        /// <summary>
        /// Override this to provide specific host change code
        /// </summary>
        protected virtual void OnHostChanged(HostChangedEventArgs e)
        {
        }

        #endregion

        #region "SESSION ENDED"

        /// <summary>
        /// Handle a SESSION ENDED notification from the network session
        /// </summary>
        void currentNetSession_SessionEnded(object sender, NetworkSessionEndedEventArgs e)
        {
            //debug info
            Engine.Instance.DebugConsole.Warn("Network Session: Session Ended! Reason: " + e.EndReason.ToString());

            //change to idle state
            this.changeState(NetServiceStates.Idle);

            if (this.currentNetSession != null)
            {
                //dispose the net session
                this.disposeSession();

                //notify client code
                this.OnSessionEnded(e);
            }
        }

        /// <summary>
        /// Override this to provide specific Session Ended code
        /// </summary>
        protected virtual void OnSessionEnded(NetworkSessionEndedEventArgs e)
        {
        }

        #endregion

        #endregion

        #region "Create Network Game"

        /// <summary>
        /// Create a new multiplayer game session
        /// </summary>
        internal void CreateGame(float waitTime)
        {

            //
            // timeout
            //

            if (waitTime > 0)
            {
                this.checkServerTimeOut = true;
                this.serverTimeOut = waitTime;
            }

            //
            // get the configuration from the base type
            //

            //get the session properties
            NetworkSessionProperties properties = this.getNetworkSessionProperties();

            //get the gamer configuration
            this.getConfigGamers(out this.configMaxLocalGamers, out this.configPrivateGamerSlots, out this.configMaxGamers);

            //get the session type
            NetworkSessionType sessionType = this.getNetworkSessionType();

            //
            // Start creating the session
            //

            //set the state
            this.changeState(NetServiceStates.CreatingGame);

            //get the gamer
            List<SignedInGamer> gamerList = new List<SignedInGamer>();
            gamerList.Add(Engine.Instance.PlayerManager.DefaultPlayer.Gamer);

            try
            {
                //start creating a session
                NetworkSession.BeginCreate(sessionType, gamerList,
                    this.configMaxGamers, this.configPrivateGamerSlots,
                    properties, OnSessionCreatedCallback, null);

                //debug
                Engine.Instance.DebugConsole.Warn("Network service: begin created executed.");
            }
            catch (Exception ex)
            {
                this.handleNetworkingException(ex);
            }
        }

        /// <summary>
        /// Create Session Callback
        /// </summary>
        private void OnSessionCreatedCallback(IAsyncResult ar)
        {
            //get the created sesssion
            try
            {
                this.createdNetSession = NetworkSession.EndCreate(ar);

                //change state
                this.changeState(NetServiceStates.InLobby);

                //debug
                Engine.Instance.DebugConsole.Warn("Network service: session created.");
            }
            catch (Exception ex)
            {
                this.handleNetworkingException(ex);
            }

        }

        #endregion

        #region "Join Game"

        internal void StartJoinGame()
        {
            //TODO: Make sure all previous used sessions are disposed

            //
            // get the configuration from the base type
            //

            this.checkServerTimeOut = false;
            this.serverTimeOut = -1;

            //get the session properties
            NetworkSessionProperties properties = this.getNetworkSessionProperties();

            //get the gamer configuration
            this.getConfigGamers(out this.configMaxLocalGamers, out this.configPrivateGamerSlots, out this.configMaxGamers);

            //get the session type
            NetworkSessionType sessionType = this.getNetworkSessionType();

            //
            // Start creating the session
            //

            //set the state
            this.changeState(NetServiceStates.SearchingGames);

            //get the gamer
            List<SignedInGamer> gamerList = new List<SignedInGamer>();
            gamerList.Add(Engine.Instance.PlayerManager.DefaultPlayer.Gamer);

            try
            {
                //start creating a session
                NetworkSession.BeginFind(sessionType, gamerList, properties, OnGamesFoundCallback, null);

                //debug
                Engine.Instance.DebugConsole.Warn("Network service: Begin find executed.");
            }
            catch (Exception ex)
            {
                this.handleNetworkingException(ex);
            }

        }

        /// <summary>
        /// Create Session Callback
        /// </summary>
        private void OnGamesFoundCallback(IAsyncResult ar)
        {
            try
            {
                //get the created sesssion
                this.foundNetSessions = NetworkSession.EndFind(ar);

                //debug
                Engine.Instance.DebugConsole.Warn("Network service: Games found: " + this.foundNetSessions.Count.ToString());

                if (this.foundNetSessions.Count == 0)
                {
                    this.OnNoGamesFound();
                }
                else
                {
                    //change state
                    this.changeState(NetServiceStates.GamesFound);

                    //
                    this.OnGamesFound();
                }
            }
            catch (Exception ex)
            {
                this.handleNetworkingException(ex);
            }

        }

        protected virtual void OnNoGamesFound()
        {
            this.changeState(NetServiceStates.Idle);
        }

        /// <summary>
        /// Default implementation when games are found, is try joining the first one
        /// </summary>
        protected virtual void OnGamesFound()
        {
            //debug
            Engine.Instance.DebugConsole.Warn("Network services: Try joining first game found");

            //take the first game
            this.JoinGame(this.foundNetSessions[0]);
        }

        /// <summary>
        /// Begin a join game session
        /// </summary>
        public void JoinGame(AvailableNetworkSession avNetSession)
        {
            //change state
            this.changeState(NetServiceStates.JoiningGame);

            try
            {
                //start a joining procedure
                NetworkSession.BeginJoin(avNetSession, OnGameJoinedCallback, null);

                //debug
                Engine.Instance.DebugConsole.Warn("Network services: begin join executed");
            }
            catch (Exception ex)
            {
                this.handleNetworkingException(ex);
            }
        }

        /// <summary>
        /// Game Join Callback
        /// </summary>
        private void OnGameJoinedCallback(IAsyncResult ar)
        {
            //commit session joining
            try
            {
                this.joinedNetSession = NetworkSession.EndJoin(ar);

                //debug
                Engine.Instance.DebugConsole.Warn("Network services: game joined!");

                //change state
                this.changeState(NetServiceStates.InLobby);
            }
            catch (Exception ex)
            {
                this.handleNetworkingException(ex);
            }
        }

        #endregion

        #region "Update"

        /// <summary>
        /// Safelly update the session
        /// </summary>
        private void updateSession()
        {
            if ((this.currentNetSession != null)
                && (!this.currentNetSession.IsDisposed))
            {
                try
                {
                    this.currentNetSession.Update();
                }
                catch (Exception ex)
                {
                    this.handleNetworkingException(ex);
                }
            }
        }

        /// <summary>
        /// Network service update: Updates current network session 
        /// </summary>
        internal void Update(float elapsedTime)
        {
            // server timeout

            if ((this.currentNetSession != null) && (this.currentNetSession.IsHost))
            {
                if (this.checkServerTimeOut)
                {
                    this.serverTimeOut -= elapsedTime;
                    if (this.serverTimeOut < 0)
                    {
                        this.OnServerTimeout();
                        this.checkServerTimeOut = false;
                    }
                }
            }

            // update states

            switch (this.currentState)
            {
                //if starting a game, just keep updating the net session
                case NetServiceStates.Starting:

                    this.updateSession();

                    break;

                //in lobby, only update the session
                case NetServiceStates.InLobby:

                    if (this.currentNetSession != null)
                    {
                        //update the network session
                        this.updateSession();

                        //update the lobby state
                        this.OnUpdateLobby(elapsedTime);
                    }

                    break;

                //in playing mode, update the session and let the 
                //client code send and receive data
                case NetServiceStates.Playing:

                    if (this.currentNetSession != null)
                    {
                        //let the client code send data 
                        this.OnUpdateSend(elapsedTime);

                        //update the network session
                        this.updateSession();

                        //let the client code receive data
                        this.OnUpdateReceive(elapsedTime);
                    }

                    break;
            }
        }

        /// <summary>
        /// Allow custom code to send data over the net
        /// </summary>
        public virtual void OnUpdateSend(float elapsedTime)
        {
        }

        /// <summary>
        /// Allow custom code to receive data over the net
        /// </summary>
        public virtual void OnUpdateReceive(float elapsedTime)
        {
        }

        /// <summary>
        /// Allow custom code to react to a server waiting timeout
        /// </summary>
        public virtual void OnServerTimeout()
        {
        }

        #region "Lobby state management"

        /// <summary>
        /// Update the lobby state
        /// </summary>
        protected virtual void OnUpdateLobby(float elapsedTime)
        {
            //if i'm the host, check for all players ready to start the game
            if ((this.currentNetSession !=  null) && (this.currentNetSession.IsHost))
            {
                //should the game be started?
                if (this.shouldStartGame())
                {
                    //starting game
                    this.changeState(NetServiceStates.Starting);

                    try
                    {
                        this.currentNetSession.StartGame();
                    }
                    catch (Exception ex)
                    {
                        this.handleNetworkingException(ex);
                    }

                }
            }
        }

        /// <summary>
        /// Override this to customize the condition to pass from Lobby to Gameplay.
        /// By default, we are ok to go when there's at least a player ready, and no unready players.
        /// </summary>
        protected virtual bool shouldStartGame()
        {
            int readyPlayers = 0;
            int unreadyPlayers = 0;

            try
            {
                foreach (NetworkGamer gamer in this.currentNetSession.RemoteGamers)
                {
                    if (gamer.IsReady) readyPlayers++;
                    else unreadyPlayers++;
                }
            }
            catch (Exception ex) 
            {
                this.handleNetworkingException(ex);
            }

            return ((readyPlayers > 0) && (unreadyPlayers == 0));
        }

        /// <summary>
        /// When the game had started
        /// </summary>
        void currentNetSession_GameStarted(object sender, GameStartedEventArgs e)
        {
            //no timeout once the game had started
            this.checkServerTimeOut = false;

            //set the logic state to "playing"
            this.changeState(NetServiceStates.Playing);
        }

        #endregion

        #endregion

        protected virtual void OnSessionCreated() { }

        protected virtual void OnGameStarted() { }

        /// <summary>
        /// Override this to provide custom reaction to networking exceptions
        /// </summary>
        protected virtual void handleNetworkingException(Exception ex)
        {
            //output the error to console
            Engine.Instance.DebugConsole.Warn("Network Service: ERROR " + ex.Message);

            //dispose the session
            this.disposeSession();

            //set idle state
            this.changeState(NetServiceStates.Idle);
        }

        #region "Aborting session"

        /// <summary>
        /// Suspend the service operation
        /// </summary>
        internal void Abort()
        {
            this.disposeSession();
        }

        #endregion
    }

}
